package com.anasoft.os.s4j;

import java.net.BindException;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;

import org.openqa.selenium.server.RemoteControlConfiguration;
import org.openqa.selenium.server.SeleniumServer;

import com.anasoft.os.s4j.annotation.EmbeddedSeleniumServer;
import com.anasoft.os.s4j.annotation.SeleniumSession;
import com.thoughtworks.selenium.Selenium;

/**
 * Manages {@link Selenium} sessions and embedded Selenium
 * servers used by Selenium tests.
 * 
 * @see SeleniumSession
 * @see EmbeddedSeleniumServer
 * 
 * @author vojtech.szocs
 */
public final class SeleniumSessionManager {

    // map of session IDs and Selenium session references
    private static final ThreadLocal<Map<String, Selenium>> sessions = new ThreadLocal<Map<String, Selenium>>();
    
    // set of ports used by embedded Selenium servers
    private static final ThreadLocal<Set<Integer>> activeServerPorts = new ThreadLocal<Set<Integer>>();
    
    static {
        sessions.set(new HashMap<String, Selenium>());
        activeServerPorts.set(new HashSet<Integer>());
    }
    
    private SeleniumSessionManager() { throw new AssertionError(); }
    
    private static void addSession(String sessionId, Selenium selenium) {
        sessions.get().put(sessionId, selenium);
    }
    
    private static void removeSession(String sessionId) {
        sessions.get().remove(sessionId);
    }
    
    private static void addServer(int serverPort) {
        activeServerPorts.get().add(serverPort);
    }
    
    public static boolean sessionActive(String sessionId) {
        return sessions.get().get(sessionId) != null;
    }
    
    public static boolean serverActive(int serverPort) {
        return activeServerPorts.get().contains(serverPort);
    }
    
    /**
     * Returns a {@link Selenium} session for the given <tt>sessionId</tt>.
     * <p>
     * Throws a {@link RuntimeException} in case the requested session
     * is not active.
     * 
     * @param sessionId Selenium session ID.
     * @return {@link Selenium} session for the given <tt>sessionId</tt>.
     */
    public static Selenium getSession(String sessionId) {
        if (sessionActive(sessionId))
            return sessions.get().get(sessionId);
        else
            throw new RuntimeException(
                    "Selenium session not active: " + sessionId);
    }
    
    /**
     * Starts a new {@link Selenium} session for the given session
     * configuration and returns its ID for further reference.
     * <p>
     * The session is stored in the session map and can be accessed
     * by calling the {@link #getSession(String) getSession} method.
     * 
     * @param sessionConfig {@link Selenium} session configuration.
     * @return Selenium session ID.
     */
    public static String startSession(SeleniumSession sessionConfig) {
        startEmbeddedServerIfNecessary(
                sessionConfig.embeddedServer(), sessionConfig.serverPort());
        
        SessionAwareDefaultSelenium selenium = new SessionAwareDefaultSelenium(
                sessionConfig.serverHost(), sessionConfig.serverPort(),
                sessionConfig.browserString(), sessionConfig.browserUrl());
        
        selenium.start();
        
        selenium.setTimeout(String.valueOf(sessionConfig.commandTimeoutInSeconds() * 1000));
        selenium.setSpeed(String.valueOf(sessionConfig.executionSpeedInMillis()));
        
        String sessionId = selenium.getSessionId();
        
        addSession(sessionId, selenium);
        
        return sessionId;
    }
    
    /**
     * Starts a new {@link SeleniumServer} instance if the given server
     * configuration tells us to do so.
     * <p>
     * The server is started only if the requested port isn't used up
     * by another server.
     * 
     * @param serverConfig {@link SeleniumServer} configuration.
     * @param serverPort Port on which the server should be running.
     */
    private static void startEmbeddedServerIfNecessary(EmbeddedSeleniumServer serverConfig, int serverPort) {
        if (serverConfig.enabled() && !serverActive(serverPort)) {
            RemoteControlConfiguration rcConfig = new RemoteControlConfiguration();
            
            rcConfig.setPort(serverPort);
            rcConfig.setSingleWindow(serverConfig.singleWindow());
            rcConfig.setTrustAllSSLCertificates(serverConfig.trustAllSSLCertificates());
            rcConfig.dontTouchLogging();
            
            try {
                // server shutdown is performed via its built-in VM shutdown hook
                SeleniumServer server = new SeleniumServer(
                        serverConfig.slowStaticResources(), rcConfig);
                
                server.start();
            }
            
            catch (BindException bex) {
                // server address/port already in use, nothing to do here
            }
            
            catch (Exception ex) {
                throw new RuntimeException("Unable to start embedded Selenium server", ex);
            }
            
            addServer(serverPort);
        }
    }
    
    /**
     * Stops a {@link Selenium} session for the given <tt>sessionId</tt>
     * and removes it from the session map.
     * 
     * @param sessionId Selenium session ID.
     */
    public static void stopSession(String sessionId) {
        getSession(sessionId).stop();
        removeSession(sessionId);
    }
    
    /**
     * Executes a <tt>callback</tt> inside new {@link Selenium} session
     * started for the given session configuration.
     * <p>
     * This is the preferred way of using Selenium sessions in tests,
     * promoting session storage inside a thread-local variable.
     * <p>
     * The session is active during <tt>callback</tt> execution. Code
     * inside the <tt>callback</tt> can access the session via the
     * <tt>sessionHolder</tt>.
     * 
     * @param sessionConfig {@link Selenium} session configuration.
     * @param sessionHolder Thread-local variable in which the session
     * should be stored during <tt>callback</tt> execution.
     * @param callback Operations intended to be executed within
     * the {@link Selenium} session.
     * @param sessionListener {@link SeleniumSessionListener}
     * implementation or <tt>null</tt>.
     */
    public static void doInSession(SeleniumSession sessionConfig, ThreadLocal<Selenium> sessionHolder,
            Runnable callback, SeleniumSessionListener sessionListener) {
        String sessionId = SeleniumSessionManager.startSession(sessionConfig);
        Selenium selenium = getSession(sessionId);
        sessionHolder.set(selenium);
        
        try {
            sessionListener.afterSessionStarted(selenium);
            callback.run();
            sessionListener.beforeSessionStopped(selenium);
        }
        
        catch (Exception ex) {
            throw new RuntimeException("Test execution failure", ex);
        }
        
        finally {
            SeleniumSessionManager.stopSession(sessionId);
            sessionHolder.set(null);
        }
    }
    
}
